luci-mod-status,luci-mod-network: support oui to vendor resolving
authorChristian Korber <[email protected]>
Wed, 10 Sep 2025 06:30:22 +0000 (08:30 +0200)
committerPaul Donald <[email protected]>
Thu, 25 Sep 2025 11:25:33 +0000 (13:25 +0200)
For easier definition of connected devices, this commit adds support
to identify them by vendor name.

Package `upf-neigh` is needed to lookup the vendor name in a hash table.
It implements a ubus-call `ubus call fingerprint fingerprint`:

```
root@<redacted> ~ # ubus call fingerprint fingerprint
{
        "7c:c2:55:XX:XX:XX": {
                "vendor": "Super Micro Computer, Inc."
        }
}
```

Fixes: #2065
Depends on: openwrt/packages#27257

Signed-off-by: Christian Korber <[email protected]>
modules/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js
modules/luci-mod-network/root/usr/share/rpcd/acl.d/luci-mod-network.json
modules/luci-mod-status/htdocs/luci-static/resources/view/status/routes.js
modules/luci-mod-status/root/usr/share/rpcd/acl.d/luci-mod-status.json

index d20c6c111f99623c3a60d043d3b94b299517d7d6..2e897ac0097f826b53e652c1213529935e85fba7 100644 (file)
@@ -11,6 +11,7 @@
 'require tools.dnsrecordhandlers as drh';
 
 var callHostHints, callDUIDHints, callDHCPLeases, CBILeaseStatus, CBILease6Status;
+var checkUfpInstalled, callUfpList;
 
 callHostHints = rpc.declare({
        object: 'luci-rpc',
@@ -30,6 +31,18 @@ callDHCPLeases = rpc.declare({
        expect: { '': {} }
 });
 
+checkUfpInstalled = rpc.declare({
+       object: 'file',
+       method: 'stat',
+       params: [ 'path' ]
+});
+
+callUfpList = rpc.declare({
+       object: 'fingerprint',
+       method: 'fingerprint',
+       expect: { '': {} }
+});
+
 CBILeaseStatus = form.DummyValue.extend({
        renderWidget: function(section_id, option_id, cfgvalue) {
                return E([
@@ -276,12 +289,19 @@ function validateMACAddr(pools, sid, s) {
 return view.extend({
        load: function() {
                return Promise.all([
-                       callHostHints(),
-                       callDUIDHints(),
-                       getDHCPPools(),
-                       network.getNetworks(),
-                       uci.load('firewall')
-               ]);
+                       checkUfpInstalled('/usr/sbin/ufpd')
+               ]).then(data => {
+                       var promises = [
+                               callHostHints(),
+                               callDUIDHints(),
+                               getDHCPPools(),
+                               network.getNetworks(),
+                               data[0].type === 'file' ? callUfpList() : null,
+                               uci.load('firewall')
+                       ]
+
+                       return Promise.all(promises);
+               });
        },
 
        render: function(hosts_duids_pools) {
@@ -290,6 +310,7 @@ return view.extend({
                    duids = hosts_duids_pools[1],
                    pools = hosts_duids_pools[2],
                    networks = hosts_duids_pools[3],
+                   macdata = hosts_duids_pools[4],
                    m, s, o, ss, so, dnss;
 
                let noi18nstrings = {
@@ -1249,16 +1270,46 @@ return view.extend({
                so.rmempty  = true;
                so.cfgvalue = function(section) {
                        var macs = uci.get('dhcp', section, 'mac');
+                       var formattedMacs;
+                       var hint, entry;
+
                        if(!Array.isArray(macs)){
-                               return expandAndFormatMAC(L.toArray(macs));
+                               formattedMacs = expandAndFormatMAC(L.toArray(macs));
                        } else {
-                               return expandAndFormatMAC(macs);
+                               formattedMacs = expandAndFormatMAC(macs);
+                       }
+
+                       if (!macdata) {
+                               return formattedMacs;
                        }
+
+
+                       if (Array.isArray(formattedMacs)){
+                               for (let mac in formattedMacs) {
+                                       entry = formattedMacs[mac].toLowerCase();
+                                       if (macdata[entry]) {
+                                               hint = macdata[entry].vendor ? macdata[entry].vendor : null;
+                                               formattedMacs[mac] += ` (${hint})`;
+                                       }
+                               }
+                               return formattedMacs;
+                       }
+
+                       if (formattedMacs) {
+                               entry = formattedMacs[0].toLowerCase();
+                               hint = macdata[entry].vendor ? macdata[entry].vendor : null;
+                               formattedMacs[0] += ` (${hint})`;
+                       }
+                       return formattedMacs;
                };
                //removed jows renderwidget function which hindered multi-mac entry
                so.validate = validateMACAddr.bind(so, pools);
                Object.keys(hosts).forEach(function(mac) {
-                       var hint = hosts[mac].name || L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0];
+                       var vendor;
+                       var lower_mac = mac.toLowerCase();
+                       if (macdata)
+                               vendor = macdata[lower_mac] ? macdata[lower_mac].vendor : null;
+                       const hint = vendor || hosts[mac].name || L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0];
                        so.value(mac, hint ? '%s (%s)'.format(mac, hint) : mac);
                });
 
@@ -1365,6 +1416,7 @@ return view.extend({
                                        cbi_update_table(mapEl.querySelector('#lease_status_table'),
                                                leases.map(function(lease) {
                                                        var exp;
+                                                       var vendor;
 
                                                        if (lease.expires === false)
                                                                exp = E('em', _('unlimited'));
@@ -1373,6 +1425,13 @@ return view.extend({
                                                        else
                                                                exp = '%t'.format(lease.expires);
 
+                                                       for (let mac in macdata) {
+                                                               if (mac.toUpperCase() === lease.macaddr) {
+                                                                       vendor = macdata[mac].vendor ?
+                                                                               ` (${macdata[mac].vendor})` : null;
+                                                               }
+                                                       }
+
                                                        var hint = lease.macaddr ? hosts[lease.macaddr] : null,
                                                            name = hint ? hint.name : null,
                                                            host = null;
@@ -1385,7 +1444,7 @@ return view.extend({
                                                        return [
                                                                host || '-',
                                                                lease.ipaddr,
-                                                               lease.macaddr,
+                                                               vendor ? lease.macaddr + vendor : lease.macaddr,
                                                                exp
                                                        ];
                                                }),
index 002a6d085d755a6523234a977167c19e4d00cd51..42181f6ee22e80fbc12f9a3124583853a46c4c48 100644 (file)
@@ -38,7 +38,9 @@
                "description": "Grant access to DHCP configuration",
                "read": {
                        "ubus": {
-                               "luci-rpc": [ "getDHCPLeases", "getDUIDHints", "getHostHints" ]
+                               "luci-rpc": [ "getDHCPLeases", "getDUIDHints", "getHostHints" ],
+                               "fingerprint": [ "fingerprint" ],
+                               "file": [ "stat" ]
                        },
                        "uci": [ "dhcp" ]
                },
index dfe19fc70aee8d76f656220e6320be33e390da0c..62a3debc87c7b9ab75f5191e5be173e32c48467e 100644 (file)
@@ -11,6 +11,18 @@ var callNetworkInterfaceDump = rpc.declare({
        expect: { interface: [] }
 });
 
+var checkUfpInstalled = rpc.declare({
+       object: 'file',
+       method: 'stat',
+       params: [ 'path' ]
+});
+
+var callUfpList = rpc.declare({
+       object: 'fingerprint',
+       method: 'fingerprint',
+       expect: { '': {} }
+});
+
 function applyMask(addr, mask, v6) {
        var words = v6 ? validation.parseIPv6(addr) : validation.parseIPv4(addr);
        var bword = v6 ? 0xffff : 0xff;
@@ -32,14 +44,21 @@ function applyMask(addr, mask, v6) {
 return view.extend({
        load: function() {
                return Promise.all([
-                       callNetworkInterfaceDump(),
-                       L.resolveDefault(fs.exec('/sbin/ip', [ '-4', 'neigh', 'show' ]), {}),
-                       L.resolveDefault(fs.exec('/sbin/ip', [ '-4', 'route', 'show', 'table', 'all' ]), {}),
-                       L.resolveDefault(fs.exec('/sbin/ip', [ '-4', 'rule', 'show' ]), {}),
-                       L.resolveDefault(fs.exec('/sbin/ip', [ '-6', 'neigh', 'show' ]), {}),
-                       L.resolveDefault(fs.exec('/sbin/ip', [ '-6', 'route', 'show', 'table', 'all' ]), {}),
-                       L.resolveDefault(fs.exec('/sbin/ip', [ '-6', 'rule', 'show' ]), {})
-               ]);
+                       checkUfpInstalled('/usr/sbin/ufpd')
+               ]).then(data => {
+                       var promises = [
+                               callNetworkInterfaceDump(),
+                               L.resolveDefault(fs.exec('/sbin/ip', [ '-4', 'neigh', 'show' ]), {}),
+                               L.resolveDefault(fs.exec('/sbin/ip', [ '-4', 'route', 'show', 'table', 'all' ]), {}),
+                               L.resolveDefault(fs.exec('/sbin/ip', [ '-4', 'rule', 'show' ]), {}),
+                               L.resolveDefault(fs.exec('/sbin/ip', [ '-6', 'neigh', 'show' ]), {}),
+                               L.resolveDefault(fs.exec('/sbin/ip', [ '-6', 'route', 'show', 'table', 'all' ]), {}),
+                               L.resolveDefault(fs.exec('/sbin/ip', [ '-6', 'rule', 'show' ]), {}),
+                               data[0].type === 'file' ? callUfpList() : null
+                       ];
+
+                       return Promise.all(promises);
+               });
        },
 
        getNetworkByDevice(networks, dev, addr, mask, v6) {
@@ -84,7 +103,7 @@ return view.extend({
                return matching_iface;
        },
 
-       parseNeigh: function(s, networks, v6) {
+       parseNeigh: function(s, macs, networks, v6) {
                var lines = s.trim().split(/\n/),
                    res = [];
 
@@ -92,7 +111,8 @@ return view.extend({
                        var m = lines[i].match(/^([0-9a-f:.]+) (.+) (\S+) *$/),
                            addr = m ? m[1] : null,
                            flags = m ? m[2].trim().split(/\s+/) : [],
-                           state = (m ? m[3] : null) || 'FAILED';
+                           state = (m ? m[3] : null) || 'FAILED',
+                           vendor;
 
                        if (!addr || state == 'FAILED' || addr.match(/^fe[89a-f][0-9a-f]:/))
                                continue;
@@ -102,12 +122,17 @@ return view.extend({
 
                        if (!flags.lladdr)
                                continue;
+                       
+                       for (let mac in macs) {
+                               if (flags.lladdr === mac)
+                                       vendor = macs[mac].vendor;
+                       }
 
                        var net = this.getNetworkByDevice(networks, flags.dev, addr, v6 ? 128 : 32, v6);
 
                        res.push([
                                addr,
-                               flags.lladdr.toUpperCase(),
+                               vendor ? flags.lladdr.toUpperCase() + ` (${vendor})` : flags.lladdr.toUpperCase(),
                                E('span', { 'class': 'ifacebadge' }, [ net ? net : '(%s)'.format(flags.dev) ])
                        ]);
                }
@@ -115,7 +140,7 @@ return view.extend({
                return res;
        },
 
-       parseRoute: function(s, networks, v6) {
+       parseRoute: function(s, macs, networks, v6) {
                var lines = s.trim().split(/\n/),
                    res = [];
 
@@ -173,7 +198,8 @@ return view.extend({
                    ip4rule = data[3].stdout || '',
                    ip6neigh = data[4].stdout || '',
                    ip6route = data[5].stdout || '',
-                   ip6rule = data[6].stdout || '';
+                   ip6rule = data[6].stdout || '',
+                   macdata = data[7];
 
                var device_title = _('Which is used to access this %s').format(_('Target'));
                var target_title = _('Network and its mask that define the size of the destination');
@@ -235,7 +261,7 @@ return view.extend({
                        ])
                ]);
 
-               cbi_update_table(neigh4tbl, this.parseNeigh(ip4neigh, networks, false),
+               cbi_update_table(neigh4tbl, this.parseNeigh(ip4neigh, macdata, networks, false),
                        E('em', _('No entries available'))
                );
                cbi_update_table(route4tbl, this.parseRoute(ip4route, networks, false),
@@ -244,7 +270,7 @@ return view.extend({
                cbi_update_table(rule4tbl, this.parseRule(ip4rule, networks, false),
                        E('em', _('No entries available'))
                );
-               cbi_update_table(neigh6tbl, this.parseNeigh(ip6neigh, networks, true),
+               cbi_update_table(neigh6tbl, this.parseNeigh(ip6neigh, macdata, networks, true),
                        E('em', _('No entries available'))
                );
                cbi_update_table(route6tbl, this.parseRoute(ip6route, networks, true),
index 200631e97b382bd3fa34e1ef7aa46361c8a84c84..8c7dbf143d3008db265691f3dfd26848c9aa8cbd 100644 (file)
@@ -49,7 +49,8 @@
                                "/sbin/ip -[46] rule show": [ "exec" ]
                        },
                        "ubus": {
-                               "file": [ "exec" ]
+                               "file": [ "exec", "stat" ],
+                               "fingerprint": [ "fingerprint" ]
                        }
                }
        },